🛡️ Introduzione alla Sicurezza Applicativa
⚠️ Perché la Sicurezza è Fondamentale?
La sicurezza non è un optional, ma un requisito fondamentale di ogni applicazione moderna. Pensa alla sicurezza come alle fondamenta di una casa: se sono deboli, l'intera struttura è a rischio, non importa quanto sia bella l'architettura sopra.
Quando sviluppiamo un'applicazione, dobbiamo proteggere:
- 🔐 I dati degli utenti: password, informazioni personali, dati sensibili
- 💾 I dati dell'applicazione: database, file, configurazioni
- 🌐 Le comunicazioni: traffico di rete tra client e server
- ⚙️ L'applicazione stessa: da injection, exploit, accessi non autorizzati
🚨 Conseguenze di una Scarsa Sicurezza
Un errore di sicurezza può avere conseguenze devastanti:
- Furto di dati: password, carte di credito, dati personali degli utenti
- Perdita di fiducia: gli utenti non torneranno dopo un data breach
- Sanzioni legali: GDPR e altre normative prevedono multe salate
- Danni economici: costi di remediation, perdita di business, danni reputazionali
- Compromissione del sistema: gli attaccanti possono prendere controllo completo
📋 Cosa Studieremo in Questa Lezione
Esploreremo i 5 pilastri fondamentali della sicurezza applicativa in Python:
- 🔐 Hashing e Password Security - Come proteggere le password usando bcrypt
- 🔑 Crittografia - Simmetrica e asimmetrica per proteggere i dati
- 💉 Prevenzione SQL Injection - Proteggere il database da attacchi
- ✅ Input Validation - Validare e sanitizzare i dati degli utenti
- 🌐 HTTPS e Certificati - Comunicazioni sicure su rete
🔐 1. Hashing e Password Security
🤔 Cos'è l'Hashing?
L'hashing è un processo matematico che trasforma un input (di qualsiasi lunghezza) in un output di lunghezza fissa, chiamato hash o digest. È come creare un'impronta digitale unica di un dato: sempre la stessa per lo stesso dato, ma impossibile da invertire.
"Ciao123!"
(bcrypt)
$2b$12$KIXj...
⚠️ Il processo è unidirezionale: dall'hash non si può risalire alla password originale!
📌 Caratteristiche Fondamentali dell'Hashing
- ✓ Deterministico: lo stesso input produce sempre lo stesso hash
- ✓ Unidirezionale: impossibile (o estremamente difficile) risalire all'input dall'hash
- ✓ Sensibile ai cambiamenti: anche una minima modifica dell'input produce un hash completamente diverso
- ✓ Dimensione fissa: l'output ha sempre la stessa lunghezza, indipendentemente dall'input
⚠️ Hashing ≠ Crittografia
Attenzione! L'hashing e la crittografia sono due cose diverse:
| Aspetto | Hashing | Crittografia |
|---|---|---|
| Reversibilità | ❌ Unidirezionale (non reversibile) | ✅ Bidirezionale (reversibile con chiave) |
| Uso principale | Verifica integrità, password | Protezione dati sensibili |
| Output | Sempre stessa lunghezza | Dipende dall'algoritmo |
| Esempio | bcrypt, SHA-256 | AES, RSA |
🔑 Perché NON Salvare Password in Chiaro
❌ MAI Fare Questo
ERRORE GRAVISSIMO - Salvare le password in chiaro nel database:
# ❌ PERICOLOSISSIMO - NON FARE MAI! class User: def __init__(self, username, password): self.username = username self.password = password # Password in chiaro! def check_password(self, password): return self.password == password # Confronto diretto # Nel database # | ID | USERNAME | PASSWORD | # |----|----------|-------------| # | 1 | mario | MarioRossi1 | ⚠️ Visibile a tutti! # | 2 | anna | Anna@2024 | ⚠️ Visibile a tutti!
Problemi:
- Se il database viene violato, tutte le password sono esposte
- Chiunque abbia accesso al database (anche gli amministratori) può vedere le password
- Gli utenti spesso usano la stessa password su più siti (credential stuffing)
- Violazione GDPR e altre normative sulla privacy
✅ La Soluzione: Hashing con bcrypt
🎯 bcrypt: Lo Standard per le Password
bcrypt è un algoritmo di hashing progettato specificamente per le password. Include funzionalità avanzate che lo rendono estremamente sicuro:
- Salt automatico: ogni password ha un "sale" casuale unico
- Computazionalmente costoso: rende gli attacchi brute-force impraticabili
- Configurabile: puoi aumentare il "costo" (rounds) per renderlo più lento
- Resistente agli attacchi: rainbow tables inutili grazie al salt
Il salt è una stringa casuale aggiunta alla password prima dell'hashing
"ciao123" → abc4b2a
"ciao123" → abc4b2a
Hash identici!
"ciao123" + aB3$ → $2b$12$7eX...
"ciao123" + 9p!Q → $2b$12$kN8...
Hash diversi!
💻 Installazione e Uso di bcrypt
Esempio Base: Hashing di una Password
import bcrypt # 1. Creare un hash da una password password = "MiaSuperPassword123!" # Convertire la stringa in bytes (bcrypt richiede bytes) password_bytes = password.encode('utf-8') # Generare il salt e creare l'hash # Il numero 12 indica il "cost factor" (numero di rounds) # Più alto = più sicuro ma più lento (valore tipico: 10-14) hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt(rounds=12)) print(f"Password originale: {password}") print(f"Hash generato: {hashed}") # Output: b'$2b$12$KIXJLqvXhZ...' (sempre diverso ad ogni esecuzione!) # 2. Verificare una password # Simuliamo l'input dell'utente al login input_password = "MiaSuperPassword123!" input_bytes = input_password.encode('utf-8') # Verifica se la password corrisponde all'hash if bcrypt.checkpw(input_bytes, hashed): print("✅ Password corretta!") else: print("❌ Password errata!") # 3. Esempio con password sbagliata wrong_password = "PasswordSbagliata" wrong_bytes = wrong_password.encode('utf-8') if bcrypt.checkpw(wrong_bytes, hashed): print("✅ Password corretta!") else: print("❌ Password errata!") # Questo verrà stampato
✅ Best Practices per Password Security
- Usa sempre bcrypt (o argon2, scrypt) per le password - mai MD5 o SHA-1
- Non salvare mai password in chiaro - nemmeno temporaneamente
- Usa rounds adeguati (10-14) - equilibrio tra sicurezza e performance
- Implementa rate limiting - blocca account dopo N tentativi falliti
- Richiedi password complesse - minimo 8 caratteri, mix di maiuscole/minuscole/numeri/simboli
- Usa HTTPS - le password devono viaggiare sempre su connessioni cifrate
- Implementa 2FA - autenticazione a due fattori per maggiore sicurezza
- Notifica cambi password - invia email quando la password viene modificata
📝 Quiz 1: Hashing e Password Security
🔑 2. Crittografia: Simmetrica e Asimmetrica
🔐 Cos'è la Crittografia?
La crittografia (o cifratura) è il processo di trasformare dati leggibili (plaintext) in dati incomprensibili (ciphertext) usando un algoritmo e una chiave. A differenza dell'hashing, la crittografia è reversibile: con la chiave giusta, puoi decifrare i dati e tornare al plaintext.
"Messaggio"
+ Chiave
"aXf9Kq..."
"aXf9Kq..."
+ Chiave
"Messaggio"
🔑 Crittografia Simmetrica
📖 Concetto
Nella crittografia simmetrica, la stessa chiave viene usata sia per cifrare che per decifrare i dati. È come avere una cassaforte con una chiave: usi la stessa chiave sia per chiuderla che per aprirla.
Chiave
Condivisa
Alice e Bob usano la STESSA chiave per cifrare e decifrare
✅ Vantaggi: Veloce, efficiente per grandi quantità di dati
❌ Svantaggi: Problema della distribuzione della chiave, stessa chiave per tutti
💻 Implementazione con Fernet (AES)
from cryptography.fernet import Fernet class SymmetricEncryption: """Gestisce la crittografia simmetrica usando Fernet (AES-128).""" def __init__(self): self.key = None self.cipher = None def generate_key(self): """Genera una nuova chiave casuale.""" self.key = Fernet.generate_key() self.cipher = Fernet(self.key) return self.key def encrypt(self, plaintext: str) -> bytes: """Cifra un testo.""" if self.cipher is None: raise ValueError("Nessuna chiave caricata") return self.cipher.encrypt(plaintext.encode('utf-8')) def decrypt(self, ciphertext: bytes) -> str: """Decifra un testo.""" if self.cipher is None: raise ValueError("Nessuna chiave caricata") return self.cipher.decrypt(ciphertext).decode('utf-8') # Esempio di utilizzo crypto = SymmetricEncryption() key = crypto.generate_key() # Cifrare un messaggio messaggio = "Questo è un messaggio segreto! 🔒" cifrato = crypto.encrypt(messaggio) print(f"Messaggio cifrato: {cifrato}") # Decifrare il messaggio decifrato = crypto.decrypt(cifrato) print(f"Messaggio decifrato: {decifrato}")
🔐 Crittografia Asimmetrica (RSA)
📖 Concetto
Nella crittografia asimmetrica, si usano due chiavi diverse: una chiave pubblica per cifrare e una chiave privata per decifrare. È come una cassetta postale: chiunque può inserire una lettera (cifrare con la chiave pubblica), ma solo il proprietario può aprirla e leggerla (decifrare con la chiave privata).
Pubblica
di Bob
Cifrato
Bob decifra con la sua 🔑 Chiave Privata (segreta)
✅ Vantaggi: Non serve scambiare chiavi segrete, ogni utente ha la sua coppia
❌ Svantaggi: Molto più lento della simmetrica, limitato nella dimensione dei dati
🔑 Simmetrica (AES)
- ✅ Veloce
- ✅ Ideale per grandi file
- ❌ Problema distribuzione chiave
- ❌ Stessa chiave per tutti
Usa per: Cifrare file, database, backup
🔐 Asimmetrica (RSA)
- ✅ Scambio chiavi sicuro
- ✅ Firma digitale
- ❌ Molto più lenta
- ❌ Limitata nella dimensione
Usa per: Firme, certificati, scambio chiavi
📝 Quiz 2: Crittografia
💉 3. SQL Injection - Prevenzione
⚠️ Cos'è SQL Injection?
SQL Injection è una delle vulnerabilità più pericolose. Permette a un attaccante di inserire codice SQL malevolo in una query, potenzialmente ottenendo accesso completo al database, rubando dati, modificandoli o cancellandoli.
Nel 2021, SQL Injection era al 3° posto nella OWASP Top 10 (le 10 vulnerabilità web più critiche).
❌ Esempio di Codice Vulnerabile
# ❌ VULNERABILE - NON FARE MAI QUESTO! import sqlite3 def login_vulnerable(username, password): """Funzione di login VULNERABILE a SQL Injection.""" conn = sqlite3.connect('users.db') cursor = conn.cursor() # ⚠️ PERICOLOSO: Query costruita con concatenazione di stringhe query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'" cursor.execute(query) result = cursor.fetchone() conn.close() return result is not None # 🔓 ATTACCO SQL INJECTION username_attacker = "admin' OR '1'='1" password_attacker = "qualsiasi" if login_vulnerable(username_attacker, password_attacker): print("💀 ATTACCO RIUSCITO! Login senza password!") # La query diventa: # SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'qualsiasi' # Che è sempre vera perché '1'='1' è sempre vero!
username = "mario"
password = "password123"
SELECT * FROM users WHERE username = 'mario' AND password = 'password123'
(se credenziali corrette)
username = "admin' OR '1'='1"
password = "qualsiasi"
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'qualsiasi'
(sempre vero!)
✅ Prevenzione: Query Parametrizzate
🛡️ La Soluzione: Query Parametrizzate
Le query parametrizzate (o prepared statements) sono il modo corretto e sicuro di eseguire query SQL. Invece di concatenare stringhe, si usano placeholder che vengono sostituiti in modo sicuro dal database.
import sqlite3 def login_secure(username, password): """Login SICURO usando query parametrizzate.""" conn = sqlite3.connect('users.db') cursor = conn.cursor() # ✅ SICURO: Usa placeholder (?) invece di concatenazione # Il database gestisce automaticamente l'escape dei caratteri speciali query = "SELECT * FROM users WHERE username = ?" cursor.execute(query, (username,)) # Tupla di parametri result = cursor.fetchone() conn.close() return result is not None # Tentativo di SQL Injection (fallisce!) username_attacker = "admin' OR '1'='1" if login_secure(username_attacker, "qualsiasi"): print("Login riuscito") else: print("✅ Attacco bloccato! SQL Injection non funziona") # Username "admin' OR '1'='1" viene trattato come stringa letterale
✅ Best Practices Anti-SQL Injection
- SEMPRE usare query parametrizzate - Mai concatenare stringhe in SQL
- Usare ORM (SQLAlchemy, Django ORM) - Gestiscono automaticamente la sicurezza
- Validare input - Controlli aggiuntivi lato applicazione
- Principio del minimo privilegio - L'utente DB deve avere solo permessi necessari
- Web Application Firewall (WAF) - Filtri a livello di rete
- Auditing e logging - Monitora query sospette
📝 Quiz 3: SQL Injection
✅ 4. Input Validation e Sanitization
📝 Cos'è la Validazione dell'Input?
La validazione dell'input è il processo di verificare che i dati forniti dagli utenti siano corretti, sicuri e conformi alle aspettative. È come un controllo di sicurezza all'aeroporto: tutto ciò che entra deve essere ispezionato.
Perché è fondamentale:
- Previene SQL Injection, XSS e altri attacchi
- Garantisce integrità dei dati
- Migliora user experience (feedback immediato su errori)
- Previene crash e comportamenti imprevisti
⚖️ Validazione vs Sanitization
| Aspetto | Validazione | Sanitization |
|---|---|---|
| Definizione | Verifica se l'input è valido | Pulisce/modifica l'input per renderlo sicuro |
| Azione | Accetta o rifiuta | Trasforma |
| Esempio | Verifica che email contenga @ | Rimuove tag HTML da input |
| Quando | Prima dell'elaborazione | Prima del salvataggio/output |
Best Practice: Usa ENTRAMBE! Prima valida, poi sanitizza se necessario.
💻 Esempio di Validazione Input
import re class InputValidator: """Classe per validare vari tipi di input.""" @staticmethod def validate_email(email): """Valida un indirizzo email.""" pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' if not email: return False, "Email non può essere vuota" if len(email) > 254: return False, "Email troppo lunga" if not re.match(pattern, email): return False, "Formato email non valido" return True, None @staticmethod def validate_password(password): """Valida una password secondo criteri di sicurezza.""" errors = [] if len(password) < 8: errors.append("Password deve essere lunga almeno 8 caratteri") if not re.search(r'[A-Z]', password): errors.append("Password deve contenere almeno una maiuscola") if not re.search(r'[a-z]', password): errors.append("Password deve contenere almeno una minuscola") if not re.search(r'\d', password): errors.append("Password deve contenere almeno un numero") if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password): errors.append("Password deve contenere almeno un carattere speciale") return len(errors) == 0, errors # Esempio di utilizzo validator = InputValidator() # Test Email emails = ["user@example.com", "invalid.email", "user@"] for email in emails: is_valid, error = validator.validate_email(email) status = "✅" if is_valid else "❌" print(f"{status} {email}: {error or 'Valida'}") # Test Password passwords = ["Abc123!@", "short", "nouppercase123!"] for pwd in passwords: is_valid, errors = validator.validate_password(pwd) status = "✅" if is_valid else "❌" print(f"{status} {pwd}") if errors: for error in errors: print(f" - {error}")
✅ Best Practices per Input Validation
- Valida lato server - Mai fidarsi del client
- Whitelist > Blacklist - Definisci cosa è permesso, non cosa è vietato
- Valida tipo, lunghezza, formato - Controlli multipli
- Feedback chiari - Spiega all'utente cosa è sbagliato
- Sanitizza sempre l'output - Specialmente per HTML
- Usa librerie consolidate - Non reinventare la ruota
📝 Quiz 4: Input Validation
🌐 5. HTTPS e Certificati SSL/TLS
🔒 Cos'è HTTPS?
HTTPS (HTTP Secure) è la versione sicura di HTTP. Utilizza SSL/TLS (Transport Layer Security) per cifrare la comunicazione tra client e server, proteggendo i dati da intercettazione e manomissione.
❌ HTTP (Non Sicuro)
Client → [dati in chiaro] → Server
🔓 Chiunque può leggere!
- Password visibili
- Dati intercettabili
- Manipolazione possibile
✅ HTTPS (Sicuro)
Client → [dati cifrati] → Server
🔒 Solo client e server possono leggere
- Dati cifrati
- Integrità garantita
- Autenticità verificata
⚠️ Perché Serve HTTPS?
- 🔐 Confidenzialità: I dati sono cifrati e non possono essere letti da terzi
- 🛡️ Integrità: I dati non possono essere modificati durante il trasferimento
- ✅ Autenticità: Garantisce che stai comunicando con il server corretto
- 📊 SEO: Google favorisce i siti HTTPS nel ranking
- ⚖️ Compliance: Richiesto da GDPR e altre normative
- 👥 Fiducia utenti: I browser mostrano lucchetto verde
🔑 Come Funziona SSL/TLS
"Ciao Server! Supporto TLS 1.3"
"Ok! Ecco il mio certificato 📜"
Client verifica certificato ✅
Generazione chiave simmetrica 🔑
🔐 Tutto il traffico è cifrato! 🔐
📜 Certificati SSL/TLS
🎫 Cos'è un Certificato?
Un certificato SSL/TLS è un documento digitale che:
- Contiene la chiave pubblica del server
- Include informazioni sul proprietario del dominio
- È firmato da una Certificate Authority (CA) fidata
- Ha una data di scadenza
| Tipo | Validazione | Uso |
|---|---|---|
| DV (Domain Validation) | Solo dominio | Blog, siti personali |
| OV (Organization Validation) | Dominio + Organizzazione | Aziende, e-commerce |
| EV (Extended Validation) | Verifica estesa | Banche, finanza |
💻 Implementare HTTPS
🚀 Let's Encrypt (Gratis!)
Let's Encrypt è una CA gratuita che fornisce certificati SSL/TLS automatici. È la scelta migliore per la maggior parte delle applicazioni web.
✅ Best Practices HTTPS
- Usa SEMPRE HTTPS - Mai HTTP per siti in produzione
- Forza HTTPS - Redirect automatico da HTTP a HTTPS
- HSTS - HTTP Strict Transport Security header
- TLS 1.3 - Usa la versione più recente di TLS
- Certificati validi - Usa Let's Encrypt o CA fidate
- Rinnovo automatico - Certbot gestisce i rinnovi
- Mixed Content - Tutte le risorse devono essere HTTPS
📝 Quiz 5: HTTPS e SSL/TLS
📝 Riepilogo e Checklist Sicurezza
✅ Checklist di Sicurezza per le Tue Applicazioni
🔐 Password e Autenticazione
- ☐ Usa bcrypt (o argon2) per hashare le password
- ☐ Mai salvare password in chiaro
- ☐ Implementa blocco account dopo N tentativi falliti
- ☐ Richiedi password forti (8+ caratteri, mix completo)
- ☐ Implementa 2FA dove possibile
🔑 Crittografia
- ☐ Usa crittografia simmetrica (AES) per dati at rest
- ☐ Usa RSA per firme digitali e scambio chiavi
- ☐ Non inventare algoritmi custom di crittografia
- ☐ Gestisci le chiavi in modo sicuro (KMS)
💉 Database e SQL Injection
- ☐ SEMPRE usa query parametrizzate
- ☐ Preferisci ORM (SQLAlchemy, Django ORM)
- ☐ Principio del minimo privilegio per utenti DB
- ☐ Implementa logging di query sospette
✅ Input Validation
- ☐ Valida lato server (mai fidarsi del client)
- ☐ Usa whitelist invece di blacklist
- ☐ Valida tipo, lunghezza, formato di ogni input
- ☐ Sanitizza HTML per prevenire XSS
- ☐ Rate limiting per prevenire abusi
🌐 HTTPS e Comunicazioni
- ☐ Usa SEMPRE HTTPS in produzione
- ☐ Forza redirect da HTTP a HTTPS
- ☐ Implementa HSTS header
- ☐ Usa TLS 1.2 o superiore
- ☐ Certificati da CA fidate (Let's Encrypt)
- ☐ Nessun mixed content